/*
 * Licensed to the Tai Ping Jin Ke
 *
 * Copyright (c) 2022 .
 * All rights reserved.
 * 项目名称：太平金科-基础组件封装层-SpringBoot组件封装-缺省实现
 * 版权说明：本软件属太平金融科技服务(上海)有限公司所有，在未获得太平金融科技服务(上海)有限公司正式授权情况下，任何企业和个人，不能获取、阅读、安装、传播本软件涉及的任何受知识产权保护的内容。
 */
package com.taipingframework.boot.web.exception.error;

import cn.hutool.core.lang.TypeReference;
import cn.hutool.json.JSONUtil;
import com.taipingframework.utility.exception.ApplicationException;
import com.taipingframework.utility.exception.SystemInterrupterException;
import com.taipingframework.utility.exception.SystemMessageException;
import com.taipingframework.boot.web.response.result.ApiResult;
import com.taipingframework.boot.web.response.status.ApiStatusEnum;
import com.taipingframework.boot.web.support.ApiHelper;
import com.taipingframework.utility.constant.ApplicationConstant;
import com.taipingframework.utility.extend.ExceptionHelper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.web.ErrorProperties;
import org.springframework.boot.autoconfigure.web.ResourceProperties;
import org.springframework.boot.autoconfigure.web.reactive.error.DefaultErrorWebExceptionHandler;
import org.springframework.boot.web.error.ErrorAttributeOptions;
import org.springframework.boot.web.reactive.error.ErrorAttributes;
import org.springframework.context.ApplicationContext;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.lang.NonNull;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.server.RequestPredicates;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono;

import java.io.Serializable;
import java.text.MessageFormat;
import java.util.List;
import java.util.Map;
import java.util.StringJoiner;

/**
 * webflux 自定义异常处理程序：
 * 重写默认的异常处理机制，不使用SpringBoot自带的`Whitelabel Error Page`，所有后端工程全部默认返回格式统一的json报文。
 * （1）方式1：重写 AbstractErrorWebExceptionHandler
 * （2）方式2：重写 DefaultErrorWebExceptionHandler
 */
@Slf4j
public class SpringWebFluxErrorHandler extends DefaultErrorWebExceptionHandler {

    private ErrorAttributes errorAttributes;

    public SpringWebFluxErrorHandler(ErrorAttributes errorAttributes, ResourceProperties resourceProperties,
                                     ErrorProperties errorProperties, ApplicationContext applicationContext) {
        super(errorAttributes, resourceProperties, errorProperties, applicationContext);
        this.errorAttributes = errorAttributes;
    }

    @Override
    protected RouterFunction<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes) {
        return RouterFunctions.route(RequestPredicates.all(), this::renderErrorResponse);
    }

    @NonNull
    @Override
    protected Mono<ServerResponse> renderErrorResponse(ServerRequest request) {
        String requestId = request.exchange().getAttribute(ApplicationConstant.RESPONSE_IDENTITY);
        String logId = MessageFormat.format("[{0}]", requestId);

        Throwable error = errorAttributes.getError(request);
        List<Class<?>> causeList = ExceptionHelper.getCauseList(error);
        if (causeList.contains(SystemMessageException.class)) { // 因业务校验不通过而手动抛出的异常 - JSON报文
            /* *****====== 这种方式很不优雅，也不美观仅作为一种等效的实现思路 ======***** */
            // 在正常的业务编码中捕获异常后这样传递参数
            // ServerWebExchange serverWebExchange = context.get(ServerWebExchange.class);
            // serverWebExchange.getAttributes().put(ApplicationConstant.RESPONSE_API_RESULT, apiResultJson);
            // 在全局异常处理程序中这样获取被传递过来的参数
            // String errJson = String.valueOf(errorMap.getOrDefault(ApplicationConstant.RESPONSE_API_RESULT, StringUtils.EMPTY));
            String errJson = error.getMessage();
            ApiResult<Serializable> apiResult;
            if (JSONUtil.isTypeJSONObject(errJson)) {
                apiResult = JSONUtil.toBean(errJson, new TypeReference<ApiResult<Serializable>>() {
            }, false);
            log.error(logId + apiResult.getMessage(), error);
            } else {
                apiResult = ApiResult.build(requestId)
                        .setStatus(ApiStatusEnum.ERR_B0001);
                        //.setResult(errJson); // 非统一定义的业务状态与系统状态，禁止返回
                log.error(logId + errJson, error);
            }
            return ServerResponse.status(HttpStatus.OK.value())
                    .contentType(MediaType.APPLICATION_JSON)
                    .body(BodyInserters.fromValue(apiResult));
        } else if (ExceptionHelper.canTraceTo(error, SystemInterrupterException.class)) { // 因业务校验不通过而手动抛出的异常 - 业务校验不通过的状态码
            StringJoiner joiner = new StringJoiner("");
            ApiResult<Serializable> result = ApiHelper.buildResult(requestId, error.getMessage(), joiner);
            log.error(logId + joiner.toString(), error);

            return ServerResponse.status(HttpStatus.OK.value())
                    .contentType(MediaType.APPLICATION_JSON)
                    .body(BodyInserters.fromValue(result));
        }

        // 未被捕获 或者 已经捕获在重新包装后 而抛出的运行时异常
        boolean includeStackTrace = isIncludeStackTrace(request, MediaType.ALL);
        Map<String, Object> errorMap = this.getErrorAttributes(request, includeStackTrace
                ? ErrorAttributeOptions.of(ErrorAttributeOptions.Include.STACK_TRACE)
                : ErrorAttributeOptions.defaults());
        Object errMsg = errorMap.getOrDefault("trace", HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase());
        errorMap.remove("trace"); // 此处需要注意，后端的异常堆栈信息只能出现在日志记录中，而不能返回给web-api或open-api的客户端，从而造成系统安全漏洞！
        log.error(logId + errMsg, new ApplicationException(error));

        ApiResult<Serializable> result = ApiResult.build(requestId)
                .setStatus(ApiStatusEnum.ERR_B0001);
                //.setResult(new HashMap<>(errorMap)); // 非统一定义的业务状态与系统状态，禁止返回
        return ServerResponse.status(Integer.parseInt(String.valueOf(errorMap.get("status"))))
                .contentType(MediaType.APPLICATION_JSON)
                .body(BodyInserters.fromValue(result));
    }

}
